﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Security.AccessControl;
using System.Text;
using System.Threading;

namespace Sharp.Interprocess.MemoryManager
{
    public class BuddyMemoryManager : IMemoryManager,IDisposable
    {
        #region Constants
        private const string MEMORY_MANAGER_STORE_MUTEX_SUFFIX = "_MemoryManagerMutex";
        #endregion

        #region Private Members

        private int m_TotalStoreSize;
        private MemoryMappedFile m_AllocationStateStore;
        private SegmentAllocator m_SegmentAllocator;
        private SegmentDeallocator m_SegmentDeallocator;
        private string m_MemoryManagerId;
        private Mutex m_SyncObject;

        #endregion

        #region Constructor(s)
        public BuddyMemoryManager(string memoryManagerId,int totalAllocationSize)
        {            
            m_MemoryManagerId = memoryManagerId;
            m_SyncObject = new Mutex(false, m_MemoryManagerId + MEMORY_MANAGER_STORE_MUTEX_SUFFIX);
            
            m_SegmentAllocator = new SegmentAllocator(m_SyncObject);
            m_SegmentDeallocator = new SegmentDeallocator(m_SyncObject);

            var totalStorageSizePowerOf2 = AllocatorUtil.FindClosestSegmentSize(totalAllocationSize);
            m_TotalStoreSize = totalStorageSizePowerOf2;
            SegmentBlocks = new GeneralTree<SegmentBlock>(new SegmentBlock(totalStorageSizePowerOf2, 0));

            CreateOrOpenAllocationDataStore(m_TotalStoreSize);
            BuildSegmentBlockMap(SegmentBlocks);

            StoreEmptyAllocationStateDataIfNeeded(totalStorageSizePowerOf2, SegmentBlocks);            
        }
        #endregion

        #region IMemoryManager Implementation
        public string MemoryManagerId
        {
            get 
            {
                return m_MemoryManagerId;
            }
        }

        public int TotalMemorySize
        {
            get 
            {
                return m_TotalStoreSize;
            }
        }

        /// <summary>
        /// "map" of existing allocation blocks
        /// </summary>
        public GeneralTree<SegmentBlock> SegmentBlocks { get; private set; }

        public IEnumerable<SegmentHandle> AllocatedSegmentHandles
        {
            get
            {
                try
                {
                    m_SyncObject.WaitOne();
                    var allocationStateData = GetAllocationStateData();
                    return allocationStateData.AllocatedSegmentHandles;
                }
                finally
                {
                    m_SyncObject.ReleaseMutex();
                }
            }
        }

        public SegmentHandle Allocate(int allocationSize)
        {
            SegmentHandle allocatedSegmentHandle = null;
            try
            {                
                m_SyncObject.WaitOne();
                var allocationStateData = GetAllocationStateData();
                if (allocationStateData == null)
                {
                    throw new ApplicationException("Invalid allocation state data");
                }

                IEnumerable<SegmentBlock> allocatedSegmentBlocks;
                if (allocationSize <= MemoryManagerUtil.MaxSegmentSize)
                {
                    allocatedSegmentBlocks = m_SegmentAllocator.Allocate(allocationSize,
                                                                         SegmentBlocks,
                                                                         allocationStateData.AvailableBlockCountBySize,
                                                                         allocationStateData.AllocatedSegmentOffsets);
                }
                else
                {
                    allocatedSegmentBlocks = null;
                }

                allocatedSegmentHandle = new SegmentHandle(handleId: allocationStateData.AllocatedSegmentHandles.Count + 1); ;
                var partId = 0;
                foreach (var allocatedBlock in allocatedSegmentBlocks)
                {
                    allocatedSegmentHandle.Segments.Add(new Segment(allocatedBlock.BlockSize, allocatedBlock.BlockOffset) { SegmentPartId = partId++ });
                }

                allocationStateData.AllocatedSegmentHandles.Add(allocatedSegmentHandle);
                SetAllocationStateData(allocationStateData);
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }

            return allocatedSegmentHandle;
        }

        public void Free(SegmentHandle segmentHandle)
        {
            try
            {
                m_SyncObject.WaitOne();
                var allocationStateData = GetAllocationStateData();
                if (allocationStateData == null)
                {
                    throw new ApplicationException("Invalid allocation state data");
                }

                m_SegmentDeallocator.Free(segmentHandle,SegmentBlocks,allocationStateData.AvailableBlockCountBySize,allocationStateData.AllocatedSegmentOffsets);
                allocationStateData.AllocatedSegmentHandles.RemoveWhere(allocatedHandle => allocatedHandle.HandleId == segmentHandle.HandleId);
                SetAllocationStateData(allocationStateData);
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }
        }

        #endregion

        #region IDisposable Implementation
        public void Dispose()
        {
            if (m_SyncObject != null)
            {
                m_SyncObject.Dispose();
            }

            if (m_AllocationStateStore != null)
            {
                m_AllocationStateStore.Dispose();
            }
        }
        #endregion

        #region Helper Methods
        private void BuildSegmentBlockMap(GeneralTree<SegmentBlock> segmentBlockData)
        {
            var currentSegmentBlock = segmentBlockData.Data;
            if (currentSegmentBlock.BlockSize <= MemoryManagerUtil.MinSegmentSize)
            {
                return;
            }

            var newBlockSize = currentSegmentBlock.BlockSize / 2;
            var buddyBlock1 = new SegmentBlock(newBlockSize, currentSegmentBlock.BlockOffset);
            var buddyBlock2 = new SegmentBlock(newBlockSize, currentSegmentBlock.BlockOffset + newBlockSize);

            var buddyBlock1SubTree = segmentBlockData.Add(buddyBlock1);
            var buddyBlock2SubTree = segmentBlockData.Add(buddyBlock2);

            BuildSegmentBlockMap(buddyBlock1SubTree);
            BuildSegmentBlockMap(buddyBlock2SubTree);
        }

        private void StoreEmptyAllocationStateDataIfNeeded(int totalAllocationSize,GeneralTree<SegmentBlock> segmentBlocks)
        {
            try
            {
                m_SyncObject.WaitOne();
                var storedCollectionSize = m_AllocationStateStore.ReadData<int>(0);
                if (storedCollectionSize == 0) //memory mapped file is just created --> add allocation state data for first time
                {
                    var allocationStateData = new AllocationStateData(totalAllocationSize);
                    var allSegmentBlocks = segmentBlocks.ToList();
                    allSegmentBlocks.ForEach(segmentBlock =>
                        {
                            if (!allocationStateData.AvailableBlockCountBySize.ContainsKey(segmentBlock.BlockSize))
                            {
                                allocationStateData.AvailableBlockCountBySize.Add(segmentBlock.BlockSize, 0);
                            }

                            allocationStateData.AvailableBlockCountBySize[segmentBlock.BlockSize]++;                            
                        });
                    
                    SetAllocationStateData(allocationStateData);
                }
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }
        }

        private AllocationStateData GetAllocationStateData()
        {
            try
            {
                m_SyncObject.WaitOne();
                var serializedAllocationStateData = m_AllocationStateStore.ReadCollection<byte>(0).ToArray();
                if (serializedAllocationStateData.Length == 0)
                {
                    return null;
                }

                var deserializedAllocationStateData = SerializationUtil.Deserialize<AllocationStateData>(serializedAllocationStateData);
                return deserializedAllocationStateData;
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }
        }

        private void SetAllocationStateData(AllocationStateData allocationStateData)
        {
            try
            {
                m_SyncObject.WaitOne();
                var serializedAllocationStateData = SerializationUtil.Serialize(allocationStateData);
                ResizeAllocationStateStoreIfNeeded(serializedAllocationStateData.Length);
                m_AllocationStateStore.WriteCollection(0, serializedAllocationStateData);
            }
            finally
            {
                m_SyncObject.ReleaseMutex();
            }

        }

        private void ResizeAllocationStateStoreIfNeeded(int allocationStateDataSize)
        {
            if (m_TotalStoreSize <= allocationStateDataSize)
            {
                m_TotalStoreSize = allocationStateDataSize * 2;
                m_AllocationStateStore.Dispose();

                CreateOrOpenAllocationDataStore(m_TotalStoreSize);
            }
        }

        private void CreateOrOpenAllocationDataStore(int allocationStateDataSize)
        {
            m_AllocationStateStore = MemoryMappedFile.CreateOrOpen(
                m_MemoryManagerId,
                allocationStateDataSize);
        }
        #endregion
    }
}
